﻿using System.Text;

namespace How_toEval;

public class Lexer
{
    private string Tape { get; }
    private List<Token> Tokens { get; set; }
    private Dictionary<string, TokenType> OperatorMapTable { get; }
    private Dictionary<string, TokenType> KeywordMapTable { get; }
    private HashSet<char> OperatorValidInitial { get; }

    public Lexer(string tape, int pos = 0)
    {
        Tape = tape;
        Pos = pos;
        Tokens = new List<Token>(Tape.Length / 2);
        OperatorMapTable = new Dictionary<string, TokenType>
        {
            { "+", TokenType.BinaryOperator },
            { "-", TokenType.BinaryOperator },
            { "*", TokenType.BinaryOperator },
            { "/", TokenType.BinaryOperator },
            { "(", TokenType.LeftParentheses },
            { ")", TokenType.RightParentheses },
            { ":", TokenType.Colon },
            { "{", TokenType.Begin},
            { ",", TokenType.Comma },
            { "}", TokenType.End },
            { "=", TokenType.Equal },
            { ":=", TokenType.Define },
            { "=>", TokenType.Arrow }
        };

        OperatorValidInitial = new HashSet<char>()
        {
            '+', '-', '*', '/', '(', ')', ':', '{', '}', ',', '=', ':'
        };


        KeywordMapTable = new Dictionary<string, TokenType>
        {
            { "if", TokenType.If },
            { "else", TokenType.Else },
            { "fun", TokenType.Lambda }
        };
    }

    private void Advance()
    {
        if (Pos < Tape.Length)
        {
            Pos += 1;
        }
        else
        { }
    }

    private char? Peek()
    {
        if (Pos < Tape.Length - 1)
        {
            return Tape[Pos + 1];
        }
        else
        {
            return null;
        }
    }

    private char? Read() => (Pos < Tape.Count()) switch
    {
        true => Tape[Pos],
        false => null
    };

    private char? Consume()
    {
        char? currentChar = Read();
        Advance();
        return currentChar;
    }

    private void PushInteger()
    {
        Token token = new Token(
            TokenType.Integer,
            LexemeBuffer.ToString());
        Tokens.Add(token);
        LexemeBuffer.Clear();
    }


    public List<Token> Process()
    {
        while (Read() is char c)
        {
            if (char.IsDigit(c))
            {
                IntegerOrFloat();
            }
            else if (char.IsLetter(c) || c == '_')
            {
                Id();
            }
            else if (char.IsWhiteSpace(c))
            {
                WhiteSpace();
            }
            else if (OperatorValidInitial.Contains(c))
            {
                Operator();
            }
            else
            {
                throw new Exception("Unexpected character");
            }
        }
        return Tokens;
    }

    private void IntegerOrFloat()
    {
        var isfloat = false;
        while (true)
        {
            switch (Read())
            {
                case char c when char.IsDigit(c):
                    LexemeBuffer.Append(c);
                    Advance();
                    break;
                case char c when c == '.':
                    LexemeBuffer.Append(c);
                    Advance();
                    isfloat = true;
                    break;
                default:
                    if (isfloat)
                    {
                        PushFloat();
                    }
                    else
                    {
                        PushInteger();
                    }
                    return;
            }
        }
    }

    private void PushFloat()
    {
        Token token = new Token(
            TokenType.Float,
            LexemeBuffer.ToString());
        Tokens.Add(token);
        LexemeBuffer.Clear();
    }

    private void PushOperator()
    {
        var bfstr = LexemeBuffer.ToString();
        LexemeBuffer.Clear();
        if (OperatorMapTable.TryGetValue(bfstr, out var type))
        {
            Tokens.Add(new Token(type, bfstr));
        }
        else
        {
            throw new Exception($"\"{bfstr}\" looks like operator, but is not an operator");
        }
    }

    private void Operator()
    {
        char fst = Consume()!.Value;
        LexemeBuffer.Append(fst);
        switch (fst)
        {
            case ':':
                if (Read() == '=')
                {
                    LexemeBuffer.Append(Consume());
                }
                break;
            case '=':
                if (Read() == '>')
                {
                    LexemeBuffer.Append(Consume());
                }
                break;
            default:
                break;
        }
        PushOperator();
    }

    private void WhiteSpace()
    {
        while (true)
        {
            switch (Read())
            {
                case char c when char.IsWhiteSpace(c):
                    Advance();
                    break;
                default:
                    return;
            }
        }
    }

    private void Id()
    {
        ReadWhile((c) => char.IsLetterOrDigit(c) || c == '_', PushId);
    }

    void ReadWhile(Func<char, bool> continueCriteria, Action callback)
    {
        while (true)
        {
            switch (Read())
            {
                case char c when continueCriteria(c):
                    LexemeBuffer.Append(c);
                    Advance();
                    break;
                default:
                    callback();
                    return;
            }
        }
    }

    private void PushId()
    {
        string key = LexemeBuffer.ToString();
        LexemeBuffer.Clear();
        if (KeywordMapTable.TryGetValue(key, out TokenType type))
        {
            Tokens.Add(new Token(type, key));
        }
        else
        {
            Tokens.Add(new Token(TokenType.Symbol, key));
        }
    }


    protected int Pos { get; set; }
    private StringBuilder LexemeBuffer { get; set; } = new StringBuilder("", 10);
}
